Backup & Recovery
A usable Melodee backup combines a logical PostgreSQL dump with the persistent application files and the configuration needed to mount them. Copying only the container image is not a backup.
What to Protect
| Data | Default location | Required? |
|---|---|---|
| PostgreSQL database | melodee_db_data |
Yes |
| Published media and DecentDB files | melodee_storage |
Yes |
| Inbound and Staging work | melodee_inbound, melodee_staging |
If work is pending |
| Playlists and user images | melodee_playlists, melodee_user_images |
Yes |
| Podcasts, themes, and templates | Corresponding named volumes | If used |
| ASP.NET data-protection keys | melodee_data_protection_keys |
Recommended |
| Deployment configuration | .env, Compose files, proxy configuration |
Yes |
| Logs | melodee_logs |
Optional for recovery; useful for diagnosis |
The MusicBrainz and Artist Search DecentDB files live under Storage in the default container layout. They can be rebuilt, but preserving them can save substantial download and import time.
Create a Backup
Choose a destination that is not inside a Melodee volume:
backup_dir="/srv/backups/melodee/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$backup_dir"
chmod 700 "$backup_dir"
1. Dump PostgreSQL
pg_dump produces a transactionally consistent logical backup while
PostgreSQL is running:
docker compose exec -T melodee-db \
pg_dump --username=melodeeuser --dbname=melodeedb --format=custom \
> "$backup_dir/postgres.dump"
Confirm the dump can be read:
docker compose exec -T melodee-db pg_restore --list \
< "$backup_dir/postgres.dump" > /dev/null
Do not archive /var/lib/postgresql/data while PostgreSQL is running. A raw
database-volume copy is not a replacement for pg_dump unless it is made by a
database-aware snapshot procedure.
2. Stop Application Writes
Stop the application while archiving its file volumes. PostgreSQL can remain running:
docker compose stop melodee.blazor
3. Archive Persistent Volumes
The helper below archives one Docker named volume at a time:
backup_volume() {
volume_name="$1"
archive_name="$2"
docker run --rm \
--volume "${volume_name}:/source:ro" \
--volume "${backup_dir}:/backup" \
alpine:3.20 \
tar -C /source -czf "/backup/${archive_name}.tar.gz" .
}
backup_volume melodee_storage storage
backup_volume melodee_inbound inbound
backup_volume melodee_staging staging
backup_volume melodee_user_images user-images
backup_volume melodee_playlists playlists
backup_volume melodee_podcasts podcasts
backup_volume melodee_themes themes
backup_volume melodee_templates templates
backup_volume melodee_data_protection_keys data-protection-keys
If a path is a bind mount, archive or snapshot the host directory with your
normal backup system instead of using docker volume.
For Podman, replace docker with podman and confirm the volume names with
podman volume ls.
4. Save Configuration and Restart
cp --preserve=mode .env compose.yml "$backup_dir/"
if [ -f compose.override.yml ]; then
cp --preserve=mode compose.override.yml "$backup_dir/"
fi
docker compose start melodee.blazor
curl --fail http://localhost:8080/health
Treat .env and database dumps as secrets. Encrypt backup media, restrict file
permissions, and do not commit them to Git.
5. Create Checksums
cd "$backup_dir"
sha256sum postgres.dump *.tar.gz > SHA256SUMS
sha256sum --check SHA256SUMS
Copy the completed backup to another failure domain. A backup on the same disk as the live data does not protect against disk loss.
Restore a Backup
Test restores on an isolated host or Compose project before relying on the procedure in an emergency.
1. Stop Melodee
docker compose down
Do not use -v; the restore procedure reuses or deliberately replaces the
existing named volumes.
2. Restore File Volumes
Restore only into empty volumes, or explicitly remove their existing contents after confirming the volume name and backup set.
restore_volume() {
volume_name="$1"
archive_path="$2"
archive_name="$(basename "$archive_path")"
docker volume create "$volume_name" > /dev/null
docker run --rm \
--volume "${volume_name}:/target" \
--volume "$(dirname "$archive_path"):/backup:ro" \
alpine:3.20 \
sh -c 'rm -rf /target/* /target/.[!.]* /target/..?* 2>/dev/null || true; tar -C /target -xzf "/backup/$1"' \
sh "$archive_name"
}
restore_volume melodee_storage "$backup_dir/storage.tar.gz"
restore_volume melodee_inbound "$backup_dir/inbound.tar.gz"
restore_volume melodee_staging "$backup_dir/staging.tar.gz"
restore_volume melodee_user_images "$backup_dir/user-images.tar.gz"
restore_volume melodee_playlists "$backup_dir/playlists.tar.gz"
restore_volume melodee_podcasts "$backup_dir/podcasts.tar.gz"
restore_volume melodee_themes "$backup_dir/themes.tar.gz"
restore_volume melodee_templates "$backup_dir/templates.tar.gz"
restore_volume melodee_data_protection_keys "$backup_dir/data-protection-keys.tar.gz"
The cleanup inside this example is destructive to the named target volume. Run it only after verifying the target name and backup path. For bind mounts, restore the host directories with the tool that created their backups.
3. Restore PostgreSQL
Start only the database service, replace the application database, and feed the
custom-format dump to pg_restore:
docker compose up -d melodee-db
docker compose exec -T melodee-db \
dropdb --username=melodeeuser --if-exists --force melodeedb
docker compose exec -T melodee-db \
createdb --username=melodeeuser melodeedb
docker compose exec -T melodee-db \
pg_restore --username=melodeeuser --dbname=melodeedb \
--no-owner --no-privileges \
< "$backup_dir/postgres.dump"
Restore .env and Compose overrides before starting the application so the
database password, JWT signing key, paths, and image version match the backup.
4. Start and Verify
docker compose up -d
docker compose ps
docker compose logs --tail=250 melodee.blazor
curl --fail http://localhost:8080/health
Sign in, run Admin > Doctor, browse a known album, stream a song, and inspect library counts. Keep the backup untouched until verification is complete.
Backup Policy
A practical policy includes:
- Frequent PostgreSQL dumps
- File snapshots after imports or metadata curation
- A backup before every Melodee or PostgreSQL upgrade
- Multiple retention periods, such as daily, weekly, and monthly copies
- At least one encrypted off-host or offline copy
- Scheduled restore tests with recorded results
Volume archives can be large. If the canonical media library is already protected by storage snapshots, do not duplicate it blindly; document exactly which system is authoritative and ensure its restore is tested with the matching PostgreSQL backup.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.